<?php
/**
 * Talents come from diligence, and knowledge is gained by accumulation.
 *
 * @author: 晋<657306123@qq.com>
 * @date: 2019/8/5 23:19
 */

namespace Xin\Filesystem\Adapter\Aliyun;

use League\Flysystem\Config;
use OSS\Core\OssException;
use OSS\OssClient;
use Xin\Filesystem\Adapter\AbstractCloudAdapter;
use Xin\Filesystem\AdapterInterface;
use Xin\Filesystem\FilesystemException;

/**
 * 阿里云
 */
class Aliyun extends AbstractCloudAdapter{

	/**
	 * @var \OSS\OssClient
	 */
	protected $client;

	/**
	 * @var array
	 */
	protected static $resultOptionsMap = [
		'Body'          => 'contents',
		'ContentLength' => 'size',
		'ContentType'   => 'mimetype',
		'Size'          => 'size',
		'Metadata'      => 'metadata',
	];

	/**
	 * @var array
	 */
	protected static $mappingOptions = [
		'contentType'   => OssClient::OSS_CONTENT_TYPE,
		'mimetype'      => OssClient::OSS_CONTENT_TYPE,
		'contentLength' => OssClient::OSS_LENGTH,
		'size'          => OssClient::OSS_LENGTH,
		'headers'       => OssClient::OSS_HEADERS,
	];

	/**
	 * Aliyun constructor.
	 *
	 * @param array $config
	 * @throws \Xin\Filesystem\FilesystemException
	 */
	public function __construct(array $config){
		parent::__construct($config);

		try{
			$this->client = new OssClient(
				$config['ak'],
				$config['sk'],
				isset($config['endpoint']) ? $config['endpoint'] : 'http://oss-cn-hangzhou.aliyuncs.com'
			);
		}catch(OssException $e){
			throw new FilesystemException($e->getErrorMessage(), $e->getErrorCode());
		}
	}

	/**
	 * @inheritDoc
	 */
	public function getUploadToken($path = null, $expires = 3600, $policy = null, $strictPolicy = true){
		// TODO: Implement getUploadToken() method.
	}

	/**
	 * @inheritDoc
	 */
	public function getPrivateFileUrl($path, $expires = 3600){
		$object = $this->applyPathPrefix($path);

		try{
			return $this->client->signUrl(
				$this->config['bucket'],
				$object,
				$expires
			);
		}catch(OssException $e){
			$this->lastError = $e->getErrorMessage();
			return false;
		}
	}

	/**
	 * @inheritDoc
	 */
	public function write($path, $contents, Config $config){
		$object = $this->applyPathPrefix($path);
		$options = $this->resolveOptions($config);

		if($visibility = $config->get('visibility')){
			$options[OssClient::OSS_HEADERS] = [
				OssClient::OSS_OBJECT_ACL => $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private',
			];
		}

		try{
			$result = $this->client->putObject(
				$this->config['bucket'],
				$object,
				$contents,
				$options
			);
			$result = $this->normalizeFileInfo($result);
			$this->fileInfoPool->put($object, $result);

			return $result;
		}catch(OssException $e){
			$this->lastError = $e->getErrorMessage();
			return false;
		}
	}

	/**
	 * @inheritDoc
	 */
	public function read($path){
		$object = $this->applyPathPrefix($path);

		try{
			$contents = $this->client->getObject(
				$this->config['bucket'], $object
			);
		}catch(OssException $e){
			$this->lastError = $e->getErrorMessage();
			return false;
		}

		return [
			'contents' => $contents,
			'path'     => $path,
		];
	}

	/**
	 * @inheritDoc
	 */
	public function copy($path, $newpath){
		$object = $this->applyPathPrefix($path);
		$newobject = $this->applyPathPrefix($newpath);

		try{
			return $this->client->copyObject(
				$this->config['bucket'],
				$object,
				$this->config['bucket'],
				$newobject
			);
		}catch(OssException $e){
			$this->lastError = $e->getErrorMessage();
			return false;
		}
	}

	/**
	 * @inheritDoc
	 */
	public function delete($path){
		$object = $this->applyPathPrefix($path);

		try{
			return $this->client->deleteObject(
				$this->config['bucket'],
				$object
			);
		}catch(OssException $e){
			$this->lastError = $e->getErrorMessage();
			return false;
		}
	}

	/**
	 * @inheritDoc
	 */
	public function deleteDir($dirname){
		$list = $this->listContents($dirname, true);
		if(false === $list){
			return false;
		}

		$objects = [];
		foreach($list as $val){
			$path = $this->applyPathPrefix($val['path']);
			if($val['type'] !== 'file'){
				$objects[] = $path.'/';
			}

			$objects[] = $path;
		}

		try{
			$this->client->deleteObjects(
				$this->config['bucket'],
				$objects
			);
		}catch(OssException $e){
			$this->lastError = $e->getErrorMessage();
			return false;
		}

		return true;
	}

	/**
	 * @inheritDoc
	 */
	public function createDir($dirname, Config $config){
		$object = $this->applyPathPrefix($dirname);
		$options = $this->resolveOptions($config);

		try{
			$this->client->createObjectDir(
				$this->config['bucket'], $object, $options
			);
		}catch(OssException $e){
			$this->lastError = $e->getErrorMessage();
			return false;
		}

		return ['path' => $dirname, 'type' => 'dir'];
	}

	/**
	 * @inheritDoc
	 */
	public function setVisibility($path, $visibility){
		$object = $this->applyPathPrefix($path);

		try{
			$this->client->putObjectAcl(
				$this->config['bucket'],
				$object,
				$visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private'
			);
		}catch(OssException $e){
			$this->lastError = $e->getErrorMessage();
			return false;
		}

		return [
			'path'       => $path,
			'visibility' => $visibility,
		];
	}

	/**
	 * @inheritDoc
	 */
	public function has($path){
		$object = $this->applyPathPrefix($path);

		try{
			$exists = $this->client->doesObjectExist(
				$this->config['bucket'], $object
			);
		}catch(OssException $e){
			$this->lastError = $e->getErrorMessage();
			return false;
		}

		return $exists;
	}

	/**
	 * @param array $options
	 * @return array|bool
	 */
	protected function lists($options){
		try{
			$listObjectInfo = $this->client->listObjects(
				$this->config['bucket'], $options
			);
		}catch(OssException $e){
			$this->lastError = $e->getErrorMessage();
			return false;
		}

		return [
			'files' => $listObjectInfo->getObjectList(), // 文件列表
			'dirs'  => $listObjectInfo->getPrefixList()// 目录列表
		];
	}

	/**
	 * @inheritDoc
	 */
	public function listContents($directory = '', $recursive = false){
		$directory = rtrim($this->applyPathPrefix($directory), '\\/');
		if($directory){
			$directory .= '/';
		}

		$options = [
			'delimiter' => '/',
			'prefix'    => $directory,
			'max-keys'  => 1000,
			'marker'    => '',
		];

		$list = $this->lists($options);
		if(false === $list){
			return false;
		}

		$result = [];
		/** @var \OSS\Model\ObjectInfo $objectInfo */
		foreach($list['files'] as $objectInfo){
			$fileType = 'file';
			if($objectInfo->getSize() === 0 && $directory === $objectInfo->getKey()){
				$fileType = 'dir';
			}

			$result[] = [
				'type'      => $fileType,
				'path'      => $objectInfo->getKey(),
				'timestamp' => $objectInfo->getLastModified(),
				'size'      => $objectInfo->getSize(),
			];
		}

		/** @var \OSS\Model\PrefixInfo $prefixInfo */
		foreach($list['dirs'] as $prefixInfo){
			if($recursive){
				$next = $this->listContents(
					$this->removePathPrefix($prefixInfo->getPrefix()), $recursive
				);
				$result = array_merge($result, $next);
			}else{
				$result[] = [
					'type'      => 'dir',
					'path'      => $this->removePathPrefix($prefixInfo->getPrefix()),
					'timestamp' => 0,
					'size'      => 0,
				];
			}
		}

		return $result;
	}

	/**
	 * @inheritDoc
	 */
	public function getMetadata($path){
		$object = $this->applyPathPrefix($path);

		return $this->fileInfoPool->get($object, function($object){
			try{
				$result = $this->client->getObjectMeta(
					$this->config['bucket'], $object
				);
			}catch(OssException $e){
				$this->lastError = $e->getErrorMessage();
				return false;
			}

			return $this->normalizeFileInfo($result);
		});
	}

	/**
	 * @inheritDoc
	 */
	public function getVisibility($path){
		$object = $this->applyPathPrefix($path);

		try{
			$acl = $this->client->getObjectAcl(
				$this->config['bucket'],
				$object
			);
		}catch(OssException $e){
			$this->lastError = $e->getErrorMessage();
			return false;
		}

		return [
			'visibility' => $acl == 'public-read' ?
				AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE,
		];
	}

	/**
	 * @param mixed  $stats
	 * @param string $type
	 * @return array
	 */
	protected function normalizeFileInfo($stats, $type = 'file'){
		$url = $stats['oss-request-url'];

		if(isset($stats['content-type'])){
			$metaType = $stats['content-type'];
		}elseif(isset($stats['oss-requestheaders']['Content-Type'])){
			$metaType = $stats['oss-requestheaders']['Content-Type'];
		}else{
			$metaType = null;
		}

		if(isset($stats['content-length'])){
			$size = $stats['content-length'];
		}elseif(isset($stats['oss-requestheaders']['Content-Length'])){
			$size = $stats['oss-requestheaders']['Content-Length'];
		}else{
			$size = 0;
		}

		if(isset($stats['date'])){
			$timestamp = strtotime($stats['date']);
		}elseif(isset($stats['oss-requestheaders']['Date'])){
			$timestamp = strtotime($stats['oss-requestheaders']['Date']);
		}else{
			$timestamp = 0;
		}

		return [
			'type'      => $type,
			'path'      => substr($url, strpos($url, "/", strpos($url, "://") + 3) + 1),
			'timestamp' => $timestamp,
			'mimetype'  => $metaType,
			'size'      => $size,
			'hash'      => $stats['content-md5'],
			'url'       => $url,
		];
	}

}
